USE OF MAKEFILE IN PROJECT MANAGEMENT

Preamble

A surprising number of programmers are not aware of the various tricks that can be used to simplify the compilation of their programs. A Makefile can play a big part in this process, but many times it is sadly neglected or sometimes not in fact used at all.

Even a larger number of programmers have their source files in varying degrees of chaos, with only random copies, if any, of the older versions for reference or backup.

Most programmers have at one point or another come across the situation where an archive of their source code would have saved a lot of trouble. You might have accidentally destroyed a source file or even the whole source tree of your latest program, or maybe introduced a bug that you just can not track down and fix, but which you know wasn't in the last week's version.

Revision control is the answer to these situations. In this article I will try to explain how to use both Makefiles and revision control systems to help development of your projects.

The environment

I developed the techniques described in this article over a couple of programming projects. The development platform used is an Amiga 3000, and the software consists of the SAS/C 6.51 compiler, Heinz Wrobel's port of the RCS software (HWGRCS) and Many features of the SMakefile used as the project base are unique to SAS/C's SMake, and it very probably won't work without some changes in other environments. The ideas, however, are global, and this article should be able to give you some hints even if you use another compiler.

Makefile

Make is a tool that operates on files using a set of rules as the guide to what to do to each of them. These rules are listed in a file called the Makefile. In essence, a Make rule consists of a target file, a list of the source files on which it depends, and the instructions on how to create the target file from the source files. A simple example might look like this:

foobar:	foo.o bar.o
	slink foo.o bar.o to foobar

This is one Make rule. It says "to create 'foobar' from 'foo.o' and 'bar.o', run the command 'slink foo.o bar.o to foobar'". Make will then look for the source files and determine if 'foobar' needs to be updated or not. Make also has some built in intelligence. For example, it knows that if it does not find a file with the name ending in '.o', it should look for the corresponding file ending in '.c', and compile that file first. Thus, 'foobar' gets updated if either of the two source files 'foo.c' or 'bar.c' has been changed since the last update, and only the parts that need to be compiled will be.

RCS

RCS, short for Revision Control System, is a software package used to manage multiple versions (revisions) of files. It facilitates the storing, retrieval and merging of of versions of the same file, letting the user log the changes. It is useful as the storage system of various types of files that are revised frequently. Program source code is only one, although an obvious example.

RCS can also be used in an environment where many people use the same source tree, letting only one person have a modifiable copy of a file, thus limiting the possibilities of two persons making changes at the same time. This might sound complicated, but in effect RCS can be very simple to use.

The basic use of RCS is based around two commands, "ci" (check in) and "co" (check out). These commands let you store a file to the RCS database, and get a copy of that file out for viewing or modifications. You could, for example use "ci -r2.13 niftyprog.c" to store the source of version 2.13 of your utility program. While RCS is usable manually, it does get somewhat tiresome as the project size grows. There is also a problem with keeping RCS revision numbers and your own program version number agreeing, unless you have some automated help to handle that.

Here's where a good Makefile comes in handy. A Makefile can automatically increment the revision number after each recompile, and when you are happy with the program, you can archive the current revision with the correct number with a single command.

The SMakefile

There is no single solution to the problem of creating a Makefile that can handle not only the building of a project, but also the archival of it. I will list here a stripped down version of the template SMakefile I build my projects on. The whole version, complete with comments, is included as a separate file. In the following, the lines are numbered for reference later in the article.

1.	# SMakefile for SAS/C
2.	
3.	# Project files
4.	
5.	PROG=		xxx
6.	SRCS=		$(PROG).c
7.	OBJS=		$(PROG).o
8.	INCS=		$(PROG).h
9.	MISC=		SMakefile
10.	LIBS=
11.	DEBUGLIBS=	LIB:debug.lib
12.	
13.	# Files to delete on "smake clean"
14.	
15.	JUNK=		\#?.(o|map|lnk|gst) SCOPTIONS
16.	
17.	# Build options
18.	
19.	CFLAGS=		DEBUG=LINE OPTIMIZE NOSTACKCHECK
20.	DCFLAGS=	DEBUG=SYMBOLFLUSH NOOPTIMIZE DEF=DEBUG
21.	LDFLAGS=	STRIPDEBUG
22.	HOOKFLAGS=	NOSTACKCHECK NOSTACKEXTEND
23.	
24.	# Compiler defaults
25.	
26.	SCOPTIONS=	
27.	
28.	# Programs used
29.	
30.	MAKE=		smake
31.	VER=		ver
32.	CP=		copy
33.	CAT=		type
34.	RM=		delete quiet
35.	
36.	VERSION=	`$(CAT) VERSION`
37.	
38.	# Standard targets
39.	
40.	test:
41.		$(MAKE) "CFLAGS=$(DCFLAGS)" "LIBS=$(DEBUGLIBS)" "LDFLAGS=" $(PROG)
42.	
43.	all:		clobber $(PROG)
44.
45.	# Build rules
46.	
47.	$(PROG):	SCOPTIONS version.o $(OBJS)
48.		$(CC) version.o $(OBJS) LINK TO $@ BATCH $(LIBS) $(LDFLAGS) $(CFLAGS)
49.		$(VER) # bump revision for next link
50.	
51.	SCOPTIONS:	SMakefile
52.		$(CP) TO $@ <FROM <
53.	$(SCOPTIONS)
54.	<
55.	
56.	version.c:
57.		$(CP) TO $@ <FROM <
58.	const char VersionID[] = "$$VER: $(PROG) " VERSION " " __AMIGADATE__;
59.	const char Version[] = VERSION;
60.	const char CompileDate[] = __AMIGADATE__;
61.	<
62.		
63.	version.o:	version.c $(SRCS)
64.		$(VER) -n # make sure VERSION exists
65.		$(CC) NODEBUG DEF=VERSION="$(VERSION)" version.c
66.	
67.	rcs:
68.		ci -l$(VERSION) $(SRCS) $(INCS) $(MISC) $(DOCS)
69.
70.	clean:
71.		$(RM) $(JUNK) $(PROG)_Cat.c >NIL:
72.	
73.	clobber:	clean
74.		$(RM) $(PROG) >NIL:

Description of the SMakefile

In the beginning of the SMakefile are listed the files making up the project. The name of the program is assigned to the PROG variable, and all the object, source, and headers files are listed in OBJS, SRCS and INCS, respectively. MISC is for the other files of the project that should be archived, including the SMakefile itself. Link libraries used will be listed in the LIBS variable.

The JUNK variable contains the pattern or list of the files that are created during the build process, and can be deleted to make room.

On lines 19-22 are listed the options used in the various build stages. CFLAGS will be used when compiling the "final" version, while DCFLAGS holds the options for a "debug" compile. LDFLAGS is the option variable for the final linking. HOOKFLAGS contains the options needed for callback hooks (you will not want stack checking on here, even if you'd like it elsewhere).

The options that will be set all the time, regardless of the build mode, can be saved in SCOPTIONS. The SMakefile will automatically create the SCOPTIONS file from this variable.

Lines 30-34 list the programs used in various stages of the compilation or project management. By assigning these into variables, there is no need to repeat the actual program name in the SMakefile, and programs are more easily changed.

The VERSION variable will be set to the current program version during compilation.

Next is the default rule, which simply calls SMake again to build the project using debug settings. This way you can simply run "smake" without any options to build a debug version of your binary. Line 42 has the "all" rule, which is what you would use to create the final binary.

The rest of the SMakefile lists the standard rules for the project. These rules should not change from project to project. The included, larger SMakefile also includes rules used to create localized source from the catalog description.

The first rule creates the actual binary target file. It will simply link all the object files together, and if that was successful, bump the revision number up one so that the next compilation will get a new version. This is done using a small special utility "ver", source to which is included.

The SCOPTIONS rule recreates the SCOPTIONS file if you change SMakefile. To rebuild all source files using the new options, add SCOPTIONS to the dependency list for each source file. This dependency list (as created by Mkmk, for example) can be appended to the end of the SMakefile, or inserted anywhere within.

The next three rules are for creating the VERSION file that holds the build number, the version.c source file for the version strings you could use elsewhere in your program (use the Version[] variable for version number instead of hardcoding it in other source files), and for compiling this file.

All that is left are the three special rules for archiving the current source versions and cleaning up the object files are other "junk" cluttering up the directory. The rcs rule, on line 67, will store all the source and related files using the build number stored in the VERSION file. Note that this is one revision higher than the binary created in the last compilation. This is intentional, since the RCS operation will bump the timestamps on all the files. You would normally test the program using the debug version compiled with "smake test" (or simply "smake"), and then when you are satisfied with the operation, update the documentation etc. and call "smake rcs". This will store the files and ask you for the log information for all of them, leaving a modifiable copy of the file (updated with the new log info) in the directory. You can now call "smake all" to compile this version, and it will have the same version and revision as the one archived in the RCS.


This article is Copyright © 1995 Osma Ahvenlampi, <Osma.Ahvenlampi@hut.fi>. All rights reserved. No part of the article may be copied or distributed in electronic or printed publications without prior permission of the author. Permission hereby granted to the Amiga Report Tech Journal.